<?php

/**
 * Description of WSFormModel
 * Introducing the function attributeGroups().
 * This method is used to group certain attributes to help WSActiveForm to easily create form fields by group
 * <code>
 * <pre>
 *      public function attributeGroups() 
 *      {
 *          return array(
 *             new AttributeGroup('groupName', array(
 *                  'group1attr1',
 *                  array('group1attr2','WSActiveForm function'),
 *                  'group1attr3',
 *              ),
 *             new AttributeGroup('groupName2', array(
 *                  'group2attr1',
 *                  'group2attr2',
 *                  'group2attr3',
 *              ),
 *          );
 *      }
 * </pre>
 * <code>
 * <p>CActiveForm function is the function of WSActiveForm that will render the input of the attribute
 *
 * @author Thomas Vo
 */
class BRFormModel extends CFormModel
{
    private static $_attributeNames = array();
    private static $_attributes     = array();
    private $_attributeGroups = null;
    
    
    // ATTRIBUTE HANDLING SECTION
    
    public function __get($name) 
    {
        $this->setUpAttributes();
        if($this->hasAttribute($name))
            return self::$_attributes[get_class($this)][$name];
        else
            return parent::__get($name);
    }
    
    public function __set($name, $value)
    {
        $this->setUpAttributes();
        if($this->hasAttribute($name))
            self::$_attributes[get_class($this)][$name] = $value;
        else
            parent::__set($name, $value);
        
    }
    
    public function __isset($name) 
    {
        $this->setUpAttributes();
        if($this->hasAttribute($name) || (self::$_attributes[get_class($this)][$name] !== null))
            return true;
        else
            return parent::__isset($name);
    }
    
    public function __unset($name) 
    {
        $this->setUpAttributes();
        if($this->hasAttribute($name))
            self::$_attributes[get_class($this)][$name] = null;
        else
            parent::__unset($name);
    }
    
    protected function isAttributesSetUp()
    {
        return isset(self::$_attributes[get_class($this)]);
    }
    
    protected function setUpAttributes()
    {
        if(!$this->isAttributesSetUp())
        {
            self::$_attributes[get_class($this)] = array();
            foreach($this->attributeNames() as $attributeName)
            {
                self::$_attributes[get_class($this)][$attributeName] = null;
            }
        }
    }
    
    /**
     * 
     * @param string $attributeName
     * @return boolean whether the class has such an attribute 
     */
    public function hasAttribute($attributeName)
    {
        return array_search($attributeName, $this->attributeNames()) !== false;
    }
    
    
    
    /**
     * This method is different from getAttributeGroups(), which returns an array of group_name => group_object
     * that is built only if this method is overriden by a child class.
     * @return array an array of attribute groups and settings
     */
    public function attributeGroups()
    {
        throw new WSAttributeGroupsUndefinedException();
        return null;
    }
    
    /**
     * This method is different from attributeGroups(), which is intended to declare the available attribute groups
     * @return array an array of group_name => group_object
     * @throws WSAttributeGroupsUndefinedException 
     */
    public function getAttributeGroups()
    {
        if(!$this->attributeGroupsDefined())
            throw new WSAttributeGroupsUndefinedException();
        return $this->_attributeGroups;
    }
      
    /**
     * setup attribute groups array, an array of group_name => group_object
     */
    protected function afterConstruct() {
        parent::afterConstruct();
        if($this->attributeGroupsDefined()) 
        {  
           foreach($this->attributeGroups() as $group)
           {
               $this->_attributeGroups[$group->getName()] = $group;
           }
        }
    }
    
    
    protected function setUpAttributeNames()
    {
        
        if(!$this->isAttributeNameSetUp())
        {
            $class = new ReflectionClass(get_class($this));
            $names = array();
            foreach($class->getProperties() as $property)
            {
                $name=$property->getName();
                if($property->isPublic() && !$property->isStatic())
                    $names[] = $name;
            }
            self::$_attributeNames[get_class($this)] = $names;
        }
    }
    
    protected function isAttributeNameSetUp()
    {
        return isset(self::$_attributeNames[get_class($this)]);
    }
    
    /**
     *
     * @param CModel $model
     * @param boolean $safe defaults to false
     */
    public function importAttributeNames($model, $safe = false)
    {
        $this->setUpAttributeNames();
        $newAttributeNames = $safe ? $model->safeAttributeNames() : $model->attributeNames();
        $className         = get_class($this);
        foreach($newAttributeNames as $newAttributeName)
        {
            self::$_attributeNames[$className][] = $newAttributeName;
        }
    }
    
    
    /**
     * Returns the list of attribute names.
     * @return array;
     */
    public function attributeNames()
	{
        $this->setUpAttributeNames();
        return self::$_attributeNames[get_class($this)];
	}
    
    /**
     * 
     * @return boolean whether the user has defined attributeGroups()
     */
    public function attributeGroupsDefined()
    {
        try
        {
            $this->attributeGroups();
        }
        catch(Exception $e)
        {
            return false;
        }
        
        return true;
    }
    
    
    /**
     *
     * @param string $name
     * @return boolean whether the group exists 
     */
    public function hasAttributeGroup($name)
    {
        foreach($this->getAttributeGroups() as $group)
        {
            if($group->getName() == $name)
                return true;
        }
        return false;
    }
    
    
    /**
     *
     * @param string $name
     * @return AttributeGroup
     * @throw WSAttributeGroupNotFoundException
     */
    public function getAttributeGroup($name)
    {
        foreach($this->getAttributeGroups() as $group)
        {
            if($group->getName() == $name)
                return $group;
        }
        throw new WSAttributeGroupNotFoundException($name, get_class($this));
    }
    
    /**
     * This method can be used to get the group names in the order they were declared
     * @return boolean|array false if there's no attribute group, otherwise return an array of names in the order they were declared  
     */
    public function getAttributeGroupNames() 
    {
        
        $names = array();
        foreach($this->attributeGroups() as $group)
        {
            $names[] = $group->getName();
        }
        
        return $names;
    }
    
    
    /**
     *
     * @param string $groupName
     * @return array an assoc array of attributes and their values
     * @throw WSAttributeGroupNotFoundException
     */
    public function getAttributeValuesByGroup($groupName)
    {
        if(!$this->hasAttributeGroup($groupName))
            throw new WSAttributeGroupNotFoundException($groupName, get_class($this));
        
        $attributeNames = $this->getAttributeNamesByGroups($groupName);
        
        $attributes = array();
        foreach($attributeNames as $attributeName)
        {
            $attributes[$attributeName] = $this->$attributeName;
        }
        
        return $attributes;
    }
   
    /**
     *
     * @param string $groupName
     * @return boolean|array false if there's no such group, otherwise, an array of names
     */
    public function getAttributeNamesByGroups($groupName)
    {
        return $this->getAttributeGroup($groupName)->getAttributeNames();
    }
    
    
    
    
    /**
     * <p> If $attributeName is false(by default), return all form settings defined in attributeGroups() in an assoc array of
     * (attributeName = arrayOfSettings)
     * <p> return an array of settings for that attribute only
     * @param type $attributeName 
     * @return array an array of settings
     * @throw WSAttributeNotFoundException
     */
    public function getFormSettings($attributeName)
    {
        if(!$this->hasAttribute($attributeName))
            throw new WSAttributeNotFoundException($attributeName, get_class($this));
        foreach($this->attributeGroups() as $group)
        {
            if($currentSettings = $group->getFormSettings($attributeName))
            return $currentSettings;
        }
    }
    
  
    
    // RULES HANDLING SECTION
    
    /**
     * Merge multiple rules together and discard the rules or attributes that do not apply to this form
     * CAUTION: Sometimes rules can overlap and cause conflict, make sure you don't have any conflicting rules before merging
     * 
     * @param array $ruleArrays an array of array of rules
     * @return array the merged and filtered rules
     */
    public function mergeRules($ruleArrays)
    {
        $combinedRules = call_user_func_array('array_merge', $ruleArrays);
        return $this->filterRules($combinedRules);
    }
    
    /** 
     * <p>Filter out the attributes in the specified rules that do not apply to this form
     * <p>The following code will look through each rule in the combined rules
     * </br>At each rule it will look at the first element, which is the attribute list.
     * </br>It will split this string into attributes and discard the unneeded ones.
     * </br>It will then merge this array of valid attribute names into a string and put it back to
     * where it should be.
     *
     * @param array $rules 
     * @return array the filtered array of rules
     */
    public function filterRules($rules)
    {
        // iterate through the rule arrays
        foreach($rules as $ruleIndex => $rule)
        {
            
            // get the list of attributes
            $attributesToCheck = $rule[0];
            // split the list to make an array of attribute names
            $attributesToCheck = preg_split("/[\s,]+/", $attributesToCheck);
    
            // if the list only has one attribute
            if(count($attributesToCheck) == 1)
            {
                // if there's no such attribute in this form, then delete the rule
                if(!$this->hasAttribute($attributesToCheck[0]))
                    unset($rules[$ruleIndex]);
            }
            // if the list has more than one attribute
            if(count($attributesToCheck) > 1)
            {
                // iterate through the list of attribute to check
                foreach($attributesToCheck as $attributeIndex => $attributeToCheck)
                {
                    $attributeToCheck = $attributesToCheck[$attributeIndex];
                    
                    // if there's no such attribute in this form, then delete the attribute
                    if(!$this->hasAttribute($attributeToCheck))
                        unset($attributesToCheck[$attributeIndex]);
                }
                
                // finally, if the attributes list is empty after filtering, then delete the rule
                if(empty($attributesToCheck))
                    unset($rules[$ruleIndex]);
                // else, glue the list back to a string and update the rule
                else
                {
                    $attributesToCheck = implode(', ', $attributesToCheck);
                    $rule[0] = $attributesToCheck;
                    $rules[$ruleIndex] = $rule;  
                }
                
            }
        }
        return $rules;
    }
}

/**
 * <p>NOTE that group name and attribute names can only be defined once to make sure that we have an array of unique attribute group 
 * <p>Attribute name can either be a string or an array of the form array('attributeName','CActiveForm func', extras...).
 * <p>The form settings for an attribute is an array of the form array('CActiveForm func', extras...).
 */
class AttributeGroup
{
    private $_name            = '';
    private $_attributeNames  = array();
    private $_formSettings    = array();
    
    /**
     * Construct a group of attributes with name and a list of attribute names or arrays of the form array(name, settings).
     * Duplicate names will be ignored
     * @param string $name
     * @param array $attributes 
     */
    public function __construct($name = '', $attributeNames = array()) {
        
        $this->_name = $name;
        foreach($attributeNames as $attributeName)
        {
            if(is_string($attributeName))
                $this->_attributeNames[] = $attributeName;
            if(is_array($attributeName))
            {
                $this->_attributeNames[] = $attributeName[0];
                $this->_formSettings[$attributeName[0]] = array_slice($attributeName, 1);
            }
                
        }
    }
    
    public function getName()
    {

        return $this->_name;
    }
    
    public function getAttributeNames()
    {
        return $this->_attributeNames;
    }
    
    /**
     * The form settings for an attribute is an array of the form array('CActiveForm func', extras...).
     * @param string $attributeName
     * @return array|boolean return an assoc array of (attribute => settings) if $attributeName is false(by default), or settings for the specified attribute, 
     * or false if nothing is found for the specified attribute
     * @throw WSAttributeNotFoundException
     */
    public function getFormSettings($attributeName = false)
    {
        
        if($attributeName === false)
            return $this->_formSettings;
        
        if(!$this->hasAttribute($attributeName))
            throw new WSAttributeNotFoundException($attributeName, get_class($this).': '.$this->_name);
        
        return isset($this->_formSettings[$attributeName]) ? $this->_formSettings[$attributeName] : false;
    }
    
    /**
     * add or update form settings for the attribute
     * @param string $attributeName
     * @param array $settings
     */
    public function setFormSettings($attributeName, $settings)
    {
        if(!$this->hasAttribute($attributeName)) 
            throw new WSAttributeNotFoundException($attributeName, get_class($this).': '.$this->getName());
        
        $this->_formSettings[$attributeName] = $settings;
    }
    
    
    /**
     *
     * @param string $name
     * @return boolean whether the group has such an attribute 
     */
    public function hasAttribute($name) {
        return array_search($name, $this->getAttributeNames()) !== false;
    }
    
    /**
     *
     * @param array $names
     * @return array|boolean an array of attributes that exist in this group, false when nothing is found 
     */
    public function hasAttributes($names) {
        $intersect = array_intersect($this->getAttributeNames(), $names);
        
        return count($intersect) != 0 ? $intersect : false;  
    }
}


?>
